<template>
  <div class="visualization" ref="clear" :style="{ width: clearWindow.width + 'px', height: clearWindow.height + 'px' }"></div>
</template>

<script setup lang="ts">
import * as THREE from 'three';
import { onMounted, reactive, ref } from 'vue';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const clear = ref();
const clearWindow = reactive({
  width: 1000,
  height: 800,
});

// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);

// 坐标轴辅助
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);

// 渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true,
  logarithmicDepthBuffer: true,
});
renderer.setSize(clearWindow.width, clearWindow.height);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0xffffff, 1);

// 相机
const cameraInfo = {
  position: { x: -200, y: 100, z: 200 },
};
const camera = new THREE.PerspectiveCamera(45, clearWindow.width / clearWindow.height, 0.1, 1000);
camera.position.set(cameraInfo.position.x, cameraInfo.position.y, cameraInfo.position.z);
camera.lookAt(0, 0, 0);

// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.rotateSpeed = 0.14;

// 光照
const createLight = (x: number, y: number, z: number) => {
  const light = new THREE.DirectionalLight(0xffffff, 1);
  light.position.set(x, y, z);
  light.target.position.set(0, 0, 0);
  return light;
};
const bestLength = 30;
const lights = [
  createLight(bestLength, bestLength, -bestLength),
  createLight(-bestLength, bestLength, -bestLength),
  createLight(-bestLength, bestLength, bestLength),
  createLight(-bestLength, -bestLength, bestLength),
  createLight(bestLength, -bestLength, -bestLength),
  createLight(-bestLength, -bestLength, -bestLength),
  createLight(-bestLength, -bestLength, bestLength),
  createLight(bestLength, -bestLength, bestLength),
];
lights.forEach((light) => {
  scene.add(light);
  scene.add(light.target);
});

// 工具类
class Tool {
  posX: number = 0;
  posZ: number = 0;
  posY: number = 0;
  toolRadius: number = 0;
  toolRadiusSq: number = 0;
  roundedTip: boolean = false;

  setToolRadius(rad: number) {
    this.toolRadius = rad;
    this.toolRadiusSq = rad * rad;
  }

  getToolTouchPoint(x: number, z: number): number {
    const dx = x - this.posX;
    const dz = z - this.posZ;
    const xzDist = Math.sqrt(dx * dx + dz * dz);
    if (this.roundedTip) {
      const theta = Math.acos(xzDist / this.toolRadius);
      const yDelta = Math.sin(theta) * this.toolRadius;
      return this.posY + this.toolRadius - yDelta;
    }
    return this.posY;
  }

  isToolTouchingPoint(x: number, z: number, y: number): boolean {
    const dx = x - this.posX;
    const dz = z - this.posZ;
    const xzSq = dx * dx + dz * dz;
    if (xzSq <= this.toolRadiusSq) {
      if (y > this.posY + this.toolRadius || !this.roundedTip) {
        return true;
      } else if (this.roundedTip) {
        const dy = y - (this.posY + this.toolRadius);
        const xyzSq = xzSq + dy * dy;
        return xyzSq <= this.toolRadiusSq;
      }
    }
    return false;
  }
}

// 工件类
class Workpiece {
  xSize: number = -1;
  ySize: number = -1;
  zSize: number = -1;
  resolution: number = 1.0;
  data: Float32Array | null = null;
  meshes: THREE.Mesh[] = [];
  geometries: THREE.BufferGeometry[] = [];
  private xBits: number = 0;
  private zBits: number = 0;
  private totalBits: number = 0;
  private xChunks: number = 5;
  private zChunks: number = 5;
  private xChunkSize: number = 64;
  private zChunkSize: number = 64;
  private chunkCount: number = 0;
  private dirtyChunks: boolean[] = [];
  private material: THREE.MeshStandardMaterial;
  private halfX: number = 0;  // 工件X方向半长
  private halfZ: number = 0;  // 工件Z方向半长

  constructor() {
    this.material = new THREE.MeshStandardMaterial({
      color: 0xaaaaaa,
      side: THREE.DoubleSide,
      flatShading: true,
    });
  }

  generate(xSize: number, zSize: number, ySize: number, resolution: number) {
    this.xSize = xSize;
    this.zSize = zSize;
    this.ySize = ySize;
    this.resolution = resolution;
    this.halfX = xSize / 2;  // 计算工件X方向半长
    this.halfZ = zSize / 2;  // 计算工件Z方向半长

    this.xBits = Math.floor(this.xSize / this.resolution);
    this.zBits = Math.floor(this.zSize / this.resolution);
    this.totalBits = this.xBits * this.zBits;

    this.data = new Float32Array(this.totalBits);
    for (let i = 0; i < this.totalBits; i++) {
      this.data[i] = this.ySize;
    }

    this.xChunkSize = 64;
    this.zChunkSize = 64;
    this.xChunks = Math.ceil(this.xBits / this.xChunkSize);
    this.zChunks = Math.ceil(this.zBits / this.zChunkSize);
    this.chunkCount = this.xChunks * this.zChunks;

    this.dirtyChunks = new Array(this.chunkCount).fill(true);
    this.meshes = new Array(this.chunkCount);
    this.geometries = new Array(this.chunkCount);

    for (let i = 0; i < this.chunkCount; i++) {
      this.geometries[i] = new THREE.BufferGeometry();
      this.meshes[i] = new THREE.Mesh(this.geometries[i], this.material);
      scene.add(this.meshes[i]);
    }

    this.updateModelData(false);
  }

  getHeight(xBit: number, zBit: number): number {
    if (xBit < 0 || xBit >= this.xBits || zBit < 0 || zBit >= this.zBits) {
      return 0;
    }
    return this.data![xBit + zBit * this.xBits];
  }

  getHeightF(posX: number, posZ: number, lookAround: number): [number, number] {
    lookAround = Math.min(lookAround, this.resolution * 2);
    if (lookAround === 0) {
      // 将坐标转换为工件局部坐标系
      const bitX = Math.floor((posX + this.halfX) / this.resolution);
      const bitZ = Math.floor((posZ + this.halfZ) / this.resolution);
      return [this.getHeight(bitX, bitZ), 0];
    }

    const look = [
      [0, 0], [-1, 0], [1, 0], [0, -1], [0, 1],
      [1, 1], [-1, 1], [-1, -1], [1, -1],
    ];
    let maxH = -1;
    let maxIndex = -1;
    for (let i = 0; i < 5; i++) {
      // 将坐标转换为工件局部坐标系
      const bitX = Math.floor((posX + this.halfX + look[i][0] * lookAround) / this.resolution);
      const bitZ = Math.floor((posZ + this.halfZ + look[i][1] * lookAround) / this.resolution);
      const h = this.getHeight(bitX, bitZ);
      if (h > maxH) {
        maxH = h;
        maxIndex = i;
      }
    }
    return [maxH, maxIndex];
  }

  clamp(a: number, b: number, c: number): number {
    if (a < b) return b;
    if (a > c) return c;
    return a;
  }

  cut(tool: Tool) {
    // 将工具位置转换到工件局部坐标系
    const toolX = tool.posX + this.halfX;
    const toolZ = tool.posZ + this.halfZ;

    const minX = Math.floor((toolX - tool.toolRadius - this.resolution) / this.resolution);
    const minZ = Math.floor((toolZ - tool.toolRadius - this.resolution) / this.resolution);
    let maxX = Math.ceil((toolX + tool.toolRadius + this.resolution) / this.resolution);
    let maxZ = Math.ceil((toolZ + tool.toolRadius + this.resolution) / this.resolution);
    maxX = this.clamp(maxX, 0, this.xBits);
    maxZ = this.clamp(maxZ, 0, this.zBits);

    let changed = false;
    for (let z = minZ; z < maxZ; z++) {
      const chunkZM = Math.floor((z - 1) / this.zChunkSize);
      const chunkZ = Math.floor(z / this.zChunkSize);
      const chunkZP = Math.floor((z + 1) / this.zChunkSize);
      for (let x = minX; x < maxX; x++) {
        const chunkXM = Math.floor((x - 1) / this.xChunkSize);
        const chunkX = Math.floor(x / this.xChunkSize);
        const chunkXP = Math.floor((x + 1) / this.xChunkSize);
        // 计算网格点在中心坐标系中的位置
        const mmX = (x + 0.5) * this.resolution - this.halfX;
        const mmZ = (z + 0.5) * this.resolution - this.halfZ;

        const dx = mmX - tool.posX;
        const dz = mmZ - tool.posZ;
        const dxdz = dx * dx + dz * dz;
        if (dxdz <= tool.toolRadiusSq) {
          let height = tool.getToolTouchPoint(mmX, mmZ);
          if (height < 0) height = 0;
          if (height < this.ySize) {
            const ndex = x + z * this.xBits;
            const currentHeight = this.data![ndex];
            if (!isNaN(height) && currentHeight > 0 && height < currentHeight) {
              this.data![ndex] = height;
              changed = true;
              this.setChunkDirty(chunkX, chunkZ);
              if (chunkXP !== chunkX) this.setChunkDirty(chunkXP, chunkZ);
              if (chunkXM !== chunkX) this.setChunkDirty(chunkXM, chunkZ);
              if (chunkZP !== chunkZ) this.setChunkDirty(chunkX, chunkZP);
              if (chunkZM !== chunkZ) this.setChunkDirty(chunkX, chunkZM);
            }
          }
        }
      }
    }
    if (changed) {
      this.updateModelData(false);
    }
  }

  setChunkDirty(chunkX: number, chunkZ: number) {
    if (chunkX < 0 || chunkZ < 0 || chunkX >= this.xChunks || chunkZ >= this.zChunks) return;
    this.dirtyChunks[chunkX + chunkZ * this.xChunks] = true;
  }

  updateModelData(justMakeSquare: boolean = false) {
    const offsets = [
      [-1, 0], [1, 0], [0, -1], [0, 1],
    ];
    const faceWindings = [
      [[0, 0, 1], [0, 1, 1], [0, 1, 0], [0, 0, 0]], // X-
      [[1, 1, 1], [1, 0, 1], [1, 0, 0], [1, 1, 0]], // X+
      [[1, 0, 1], [0, 0, 1], [0, 0, 0], [1, 0, 0]], // Z-
      [[0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0]], // Z+
      [[1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0]], // Bottom Y=0
      [[1, 1, 1], [1, 0, 1], [0, 0, 1], [0, 1, 1]], // Top Y=yHeight
    ];
    const normalMap = [
      [-1, 0, 0], [1, 0, 0], [0, 0, -1], [0, 0, 1], [0, -1, 0], [0, 1, 0],
    ];
    const quadToTriangle = [0, 1, 2, 0, 2, 3];

    const faceCounts: number[] = new Array(this.chunkCount).fill(0);

    for (let chunkZ = 0, chunkIndex = 0; chunkZ < this.zChunks; chunkZ++) {
      for (let chunkX = 0; chunkX < this.xChunks; chunkX++, chunkIndex++) {
        if (!this.dirtyChunks[chunkIndex]) continue;
        let faceCount = 0;
        const startX = chunkX * this.xChunkSize;
        const startZ = chunkZ * this.zChunkSize;
        const endX = Math.min(this.xBits, startX + this.xChunkSize);
        const endZ = Math.min(this.zBits, startZ + this.zChunkSize);

        if (justMakeSquare) {
          faceCount += 6;
        } else {
          for (let z = startZ; z < endZ; z++) {
            for (let x = startX; x < endX; x++) {
              const myHeight = this.getHeight(x, z);
              if (myHeight <= 0) continue;
              for (let i = 0; i < 4; i++) {
                const h2 = this.getHeight(x + offsets[i][0], z + offsets[i][1]);
                if (myHeight > h2) faceCount++;
              }
              faceCount += 2;
            }
          }
        }
        faceCounts[chunkIndex] = faceCount;
      }
    }

    for (let chunkZ = 0, chunkIndex = 0; chunkZ < this.zChunks; chunkZ++) {
      for (let chunkX = 0; chunkX < this.xChunks; chunkX++, chunkIndex++) {
        if (!this.dirtyChunks[chunkIndex]) continue;
        this.dirtyChunks[chunkIndex] = false;

        const startX = chunkX * this.xChunkSize;
        const startZ = chunkZ * this.zChunkSize;
        const endX = Math.min(this.xBits, startX + this.xChunkSize);
        const endZ = Math.min(this.zBits, startZ + this.zChunkSize);

        const vertices: number[] = [];
        const normals: number[] = [];

        if (justMakeSquare) {
          const myHeight = this.ySize;
          const scaleX = (endX - startX) / this.xChunkSize;
          const scaleZ = (endZ - startZ) / this.zChunkSize;
          for (let i = 0; i < 6; i++) {
            for (let j = 0; j < 6; j++) {
              const v = faceWindings[i][quadToTriangle[j]];
              // 修改顶点坐标：减去半长使工件中心在原点
              vertices.push(
                this.resolution * (startX + v[0] * this.xChunkSize * scaleX) - this.halfX,
                v[2] * myHeight,
                this.resolution * (startZ + v[1] * this.zChunkSize * scaleZ) - this.halfZ
              );
              normals.push(...normalMap[i]);
            }
          }
        } else {
          for (let z = startZ; z < endZ; z++) {
            for (let x = startX; x < endX; x++) {
              const myHeight = this.getHeight(x, z);
              if (myHeight <= 0) continue;
              for (let i = 0; i < 4; i++) {
                const h2 = this.getHeight(x + offsets[i][0], z + offsets[i][1]);
                if (myHeight > h2) {
                  const c = h2;
                  const m = myHeight - h2;
                  for (let j = 0; j < 6; j++) {
                    const v = faceWindings[i][quadToTriangle[j]];
                    // 修改顶点坐标：减去半长使工件中心在原点
                    vertices.push(
                      this.resolution * (x + v[0]) - this.halfX,
                      m * v[2] + c,
                      this.resolution * (z + v[1]) - this.halfZ
                    );
                    normals.push(...normalMap[i]);
                  }
                }
              }
              for (let i = 4; i < 6; i++) {
                for (let j = 0; j < 6; j++) {
                  const v = faceWindings[i][quadToTriangle[j]];
                  // 修改顶点坐标：减去半长使工件中心在原点
                  vertices.push(
                    this.resolution * (x + v[0]) - this.halfX,
                    v[2] * myHeight,
                    this.resolution * (z + v[1]) - this.halfZ
                  );
                  normals.push(...normalMap[i]);
                }
              }
            }
          }
        }

        const geometry = this.geometries[chunkIndex];
        geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
        geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
        geometry.computeVertexNormals();
      }
    }
  }

  reset() {
    for (let i = 0; i < this.totalBits; i++) {
      this.data![i] = this.ySize;
    }
    for (let i = 0; i < this.chunkCount; i++) {
      this.dirtyChunks[i] = true;
    }
    this.updateModelData(false);
  }
}

// 初始化工件和工具
const workpiece = new Workpiece();
workpiece.generate(100, 100, 25, 0.2);
const tool = new Tool();
tool.setToolRadius(2);
tool.roundedTip = true;

function animate() {
  requestAnimationFrame(animate);
  // 修改刀具运动路径，围绕工件中心(0,0)运动
  tool.posX = Math.sin(Date.now() * 0.001) * 20;
  tool.posZ = Math.cos(Date.now() * 0.001) * 20;
  tool.posY = 10;
  workpiece.cut(tool);
  controls.update();
  renderer.render(scene, camera);
}

function init() {
  animate();
}

onMounted(() => {
  if (clear.value) {
    clear.value.appendChild(renderer.domElement);
    init();
  }
});
</script>

<style lang="less">
.visualization {
  border: 1px solid #ccc;
  margin: 10px;
}
</style>